import { ASN1 } from './types';
import { Errors } from './errors';
import Buffer from '@ohos.buffer';

///--- Globals

var InvalidAsn1Error = Errors.InvalidAsn1Error;

var DEFAULT_OPTS = {
	size: 1024,
	growthFactor: 8
};

function merge(from, to) {
	// 确保 from 和 to 都是对象
	if (typeof from !== 'object' || from === null || typeof to !== 'object' || to === null) {
		throw new TypeError('Both arguments must be objects and not null');
	}

	var keys = Object.getOwnPropertyNames(from);
	keys.forEach(function (key) {
		if (to[key]) {
			return;
		}

		var value = Object.getOwnPropertyDescriptor(from, key);
		Object.defineProperty(to, key, value);
	});
	return to;
}


///--- API

function Writer(options) {
	options = merge(DEFAULT_OPTS, options || {});

	this._buf = Buffer.alloc(options.size || 1024);
	this._size = this._buf.length;
	this._offset = 0;
	this._options = options;

	// A list of offsets in the buffer where we need to insert
	// sequence tag/len pairs.
	this._seq = [];
}

Object.defineProperty(Writer.prototype, 'buffer', {
	get: function () {
		if (this._seq.length) {
			throw InvalidAsn1Error(this._seq.length + ' unended sequence(s)');
		}

		return (this._buf.subarray(0, this._offset));
	}
});

Writer.prototype.writeByte = function (b) {
	if (typeof (b) !== 'number') {
		throw new TypeError('argument must be a Number');
	}

	this._ensure(1);
	this._buf[this._offset++] = b;
};

Writer.prototype.writeInt = function (i, tag) {
	if (!Number.isInteger(i)) {
		throw new TypeError('argument must be an integer');
	}
	if (typeof (tag) !== 'number') {
		tag = ASN1.Integer;
	}

	let bytes = [];
	while (i < -0x80 || i >= 0x80) {
		bytes.push(i & 0xff);
		i = Math.floor(i / 0x100);
	}
	bytes.push(i & 0xff);

	this._ensure(2 + bytes.length);
	this._buf[this._offset++] = tag;
	this._buf[this._offset++] = bytes.length;

	while (bytes.length) {
		this._buf[this._offset++] = bytes.pop();
	}
};

Writer.prototype.writeNull = function () {
	this.writeByte(ASN1.Null);
	this.writeByte(0x00);
};


Writer.prototype.writeEnumeration = function (i, tag) {
	if (typeof (i) !== 'number') {
		throw new TypeError('argument must be a Number');
	}
	if (typeof (tag) !== 'number') {
		tag = ASN1.Enumeration;
	}

	return this.writeInt(i, tag);
};


Writer.prototype.writeBoolean = function (b, tag) {
	if (typeof (b) !== 'boolean') {
		throw new TypeError('argument must be a Boolean');
	}
	if (typeof (tag) !== 'number') {
		tag = ASN1.Boolean;
	}

	this._ensure(3);
	this._buf[this._offset++] = tag;
	this._buf[this._offset++] = 0x01;
	this._buf[this._offset++] = b ? 0xff : 0x00;
};


Writer.prototype.writeString = function (s, tag) {
	if (typeof (s) !== 'string') {
		throw new TypeError('argument must be a string (was: ' + typeof (s) + ')');
	}
	if (typeof (tag) !== 'number') {
		tag = ASN1.OctetString;
	}

	var len = Buffer.byteLength(s);
	this.writeByte(tag);
	this.writeLength(len);
	if (len) {
		this._ensure(len);
		this._buf.write(s, this._offset);
		this._offset += len;
	}
};


Writer.prototype.writeBuffer = function (buf, tag) {
	if (!Buffer.isBuffer(buf)) {
		throw new TypeError('argument must be a buffer');
	}

	// If no tag is specified we will assume `buf` already contains tag and length
	if (typeof (tag) === 'number') {
		this.writeByte(tag);
		this.writeLength(buf.length);
	}

	if (buf.length > 0) {
		this._ensure(buf.length);
		buf.copy(this._buf, this._offset, 0, buf.length);
		this._offset += buf.length;
	}
};


Writer.prototype.writeStringArray = function (strings, tag) {
	if (!(strings instanceof Array)) {
		throw new TypeError('argument must be an Array[String]');
	}

	var self = this;
	strings.forEach(function (s) {
		self.writeString(s, tag);
	});
};

// This is really to solve DER cases, but whatever for now
Writer.prototype.writeOID = function (s, tag) {
	if (typeof (s) !== 'string') {
		throw new TypeError('argument must be a string');
	}
	if (typeof (tag) !== 'number') {
		tag = ASN1.OID;
	}

	if (!/^([0-9]+\.){0,}[0-9]+$/.test(s)) {
		throw new Error('argument is not a valid OID string');
	}

	function encodeOctet(bytes, octet) {
		if (octet < 128) {
			bytes.push(octet);
		} else if (octet < 16384) {
			bytes.push((octet >>> 7) | 0x80);
			bytes.push(octet & 0x7F);
		} else if (octet < 2097152) {
			bytes.push((octet >>> 14) | 0x80);
			bytes.push(((octet >>> 7) | 0x80) & 0xFF);
			bytes.push(octet & 0x7F);
		} else if (octet < 268435456) {
			bytes.push((octet >>> 21) | 0x80);
			bytes.push(((octet >>> 14) | 0x80) & 0xFF);
			bytes.push(((octet >>> 7) | 0x80) & 0xFF);
			bytes.push(octet & 0x7F);
		} else {
			bytes.push(((octet >>> 28) | 0x80) & 0xFF);
			bytes.push(((octet >>> 21) | 0x80) & 0xFF);
			bytes.push(((octet >>> 14) | 0x80) & 0xFF);
			bytes.push(((octet >>> 7) | 0x80) & 0xFF);
			bytes.push(octet & 0x7F);
		}
	}

	var tmp = s.split('.');
	var bytes = [];
	bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10));
	tmp.slice(2).forEach(function (b) {
		encodeOctet(bytes, parseInt(b, 10));
	});

	var self = this;
	this._ensure(2 + bytes.length);
	this.writeByte(tag);
	this.writeLength(bytes.length);
	bytes.forEach(function (b) {
		self.writeByte(b);
	});
};


Writer.prototype.writeLength = function (len) {
	if (typeof (len) !== 'number') {
		throw new TypeError('argument must be a Number');
	}

	this._ensure(4);

	if (len <= 0x7f) {
		this._buf[this._offset++] = len;
	} else if (len <= 0xff) {
		this._buf[this._offset++] = 0x81;
		this._buf[this._offset++] = len;
	} else if (len <= 0xffff) {
		this._buf[this._offset++] = 0x82;
		this._buf[this._offset++] = len >> 8;
		this._buf[this._offset++] = len;
	} else if (len <= 0xffffff) {
		this._buf[this._offset++] = 0x83;
		this._buf[this._offset++] = len >> 16;
		this._buf[this._offset++] = len >> 8;
		this._buf[this._offset++] = len;
	} else {
		throw InvalidAsn1Error('Length too long (> 4 bytes)');
	}
};

Writer.prototype.startSequence = function (tag) {
	if (typeof (tag) !== 'number') {
		tag = ASN1.Sequence | ASN1.Constructor;
	}

	this.writeByte(tag);
	this._seq.push(this._offset);
	this._ensure(3);
	this._offset += 3;
};


Writer.prototype.endSequence = function () {
	var seq = this._seq.pop();
	var start = seq + 3;
	var len = this._offset - start;

	if (len <= 0x7f) {
		this._shift(start, len, -2);
		this._buf[seq] = len;
	} else if (len <= 0xff) {
		this._shift(start, len, -1);
		this._buf[seq] = 0x81;
		this._buf[seq + 1] = len;
	} else if (len <= 0xffff) {
		this._buf[seq] = 0x82;
		this._buf[seq + 1] = len >> 8;
		this._buf[seq + 2] = len;
	} else if (len <= 0xffffff) {
		this._shift(start, len, 1);
		this._buf[seq] = 0x83;
		this._buf[seq + 1] = len >> 16;
		this._buf[seq + 2] = len >> 8;
		this._buf[seq + 3] = len;
	} else {
		throw InvalidAsn1Error('Sequence too long');
	}
};

Writer.prototype._shift = function (start, len, shift) {
	// 检查 start 是否被定义
	if (start === undefined) {
		throw new Error('Missing required parameter "start"');
	}

	// 检查 len 是否被定义
	if (len === undefined) {
		throw new Error('Missing required parameter "len"');
	}

	// 检查 shift 是否为有效值（这里假设非零或负数即为有效）
	if (shift >= 0) {
		throw new Error('Invalid shift value, must be a positive number');
	}

	// 执行实际的缓冲区操作
	this._buf.copy(this._buf, start + shift, start, start + len);
	this._offset += shift;
};

Writer.prototype._ensure = function (len) {
	// assert.ok(len);

	if (!len || len <= 0) {
		// 抛出错误或进行其他错误处理
		throw new Error('Invalid length, must be a positive number');
	}
	if (this._size - this._offset < len) {
		var sz = this._size * this._options.growthFactor;
		if (sz - this._offset < len) {
			sz += len;
		}

		var buf = Buffer.alloc(sz);
		this._buf.copy(buf, 0, 0, this._offset);
		this._buf = buf;
		this._size = sz;
	}
};


///--- Exported API

export default Writer;
